package conf2;

import conf2.model.*;
import conf2.model.EActor;
import conf2.model.EActor.Side;
import conf2.model.ETerritory;
import conf2.net.*;
import conf2.text.*;
import conf2.ui.*;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import javax.swing.JOptionPane;
import javax.swing.SwingWorker;

import static conf2.model.ETerritory.*;

class GameStarter implements PlacementHandler.Listener
{

    private final MainWindow mw;
    private final MainMenu menu;
    private final Configuration config;
    private IConnection conn = null;
    private PieceCollection pieceColl = null;
    private WorldState ws = null;
    private GameState gameState = null;

    GameStarter(MainWindow mw, MainMenu menu, Configuration config)
    {
        this.mw = mw;
        this.menu = menu;
        this.config = config;
    }

    private void setMenuItemsEnabled(boolean b)
    {
        menu.setGameAsClientEnabled(b);
        menu.setGameAsServerEnabled(b);
    }

    void startClientGame()
    {
        setMenuItemsEnabled(false);

        String name = config.getPlayerName();
        String host = config.getIP();
        int port = 11234; // TODO: don't hardcode this.
        try {
            port = Integer.valueOf(config.getPort());
        } catch (Exception ignored) {
        }

        StartClientDialog.ReturnValue ret = StartClientDialog.show(mw, name, host, port);
        if (ret == null) {
            return;
        }

        conn = createClientConnection(ret.host, ret.port);
        if (conn == null) {
            // User canceled the connection, or it failed.
            setMenuItemsEnabled(true);
            return;
        }

        // Receive game info, send name, assemble game state.
        Message m = conn.waitForNextMessage();
        if (!m.dataIsOfClass(GameInfo.class)) {
            MessageBailoutDialog.show(mw, m, GameInfo.class);
            setMenuItemsEnabled(true);
            return;
        }
        GameInfo gi = (GameInfo) m.getData();

        m = new Message(Message.Type.NAME, ret.name);
        conn.sendMessage(m);

        gameState = new GameState(false,
                gi.getServerSide(),
                ret.name,
                gi.getPlayerName(),
                gi.getGameType(),
                gi.getShelobRule(),
                gi.getSpecialCardRule());

        startGame();
    }

    void startServerGame()
    {
        setMenuItemsEnabled(false);

        String name = config.getPlayerName();
        int port = 11234; // TODO: don't hardcode this.
        try {
            port = Integer.valueOf(config.getPort());
        } catch (Exception ignored) {
        }
        ShelobRule shelobRule = config.getShelobRule();
        GameType gameType = config.getGameType();
        SpecialCardRule specialCardRule = config.getSpecialCardRule();

        StartServerDialog.ReturnValue ret = StartServerDialog.show(
                mw, name, port, gameType, specialCardRule, shelobRule);
        if (ret == null) {
            setMenuItemsEnabled(true);
            return;
        }

        Side serverSide = randomSide();

        conn = createServerConnection(port);
        if (conn == null) {
            // User canceled the connection, or it failed.
            setMenuItemsEnabled(true);
            return;
        }

        // Send game info, receive name, assemble game state.
        GameInfo gi = new GameInfo(ret.name, serverSide, ret.shelobRule, ret.gameType, ret.specialCardRule);
        Message m = new Message(Message.Type.GAME_INFO, gi);
        conn.sendMessage(m);

        m = conn.waitForNextMessage();
        if (!m.dataIsOfClass(String.class)) {
            MessageBailoutDialog.show(mw, m, String.class);
            setMenuItemsEnabled(true);
            return;
        }
        String oppName = (String) m.getData();


        gameState = new GameState(true,
                serverSide,
                ret.name,
                oppName,
                ret.gameType,
                ret.shelobRule,
                ret.specialCardRule);

        startGame();

    }

    private IConnection createServerConnection(final int port)
    {
        final WaitDialog dlg = new WaitDialog(mw, Misc.CONNECTING_CAPTION, Misc.STARTING_SERVER_LABEL, Misc.CANCEL_LABEL);
        conn = null;
        SwingWorker<IConnection, Void> w = new SwingWorker<IConnection, Void>()
        {

            @Override
            protected IConnection doInBackground() throws Exception
            {
                IConnection conn = Connection.newServer(port);
                return conn;
            }

            @Override
            protected void done()
            {
                dlg.setVisible(false);
            }
        };
        w.execute();
        dlg.setVisible(true);
        if (dlg.wasCanceled()) {
            System.out.println("Server setup canceled by user.");
            w.cancel(true);
        } else {
            try {
                conn = w.get();
            } catch (Exception ex) {
                // TODO: Better error messages!
                String msg = "Fail";
                if (ex.getCause() instanceof java.net.BindException) {
                    msg = "bindfail";
                }
                ex.printStackTrace();
                JOptionPane.showMessageDialog(mw, msg, "onoz", JOptionPane.ERROR_MESSAGE);
            }
        }
        return conn;
    }

    private IConnection createClientConnection(final String host, final int port)
    {
        final WaitDialog dlg = new WaitDialog(mw, Misc.CONNECTING_CAPTION, Misc.CONNECTING_PLEASE_WAIT_LABEL, Misc.CANCEL_LABEL);
        conn = null;
        SwingWorker<IConnection, Void> w = new SwingWorker<IConnection, Void>()
        {

            @Override
            protected IConnection doInBackground() throws Exception
            {
                IConnection conn = Connection.newClient(host, port);
                return conn;
            }

            @Override
            protected void done()
            {
                dlg.setVisible(false);
            }
        };
        w.execute();
        dlg.setVisible(true);
        if (dlg.wasCanceled()) {
            System.out.println("Client setup canceled by user.");
            w.cancel(true);
        } else {
            try {
                conn = w.get();
            } catch (Exception ex) {
                // TODO: Better error messages!
                ex.printStackTrace();
                JOptionPane.showMessageDialog(mw, "Fail", "onoz", JOptionPane.ERROR_MESSAGE);
            }
        }
        return conn;
    }

    private Side randomSide()
    {
        int rnd = (int) (Math.random() * 100);
        return (rnd < 50 ? Side.DARK : Side.LIGHT);
    }

    private void showGameInformationDialog(GameState gameState)
    {
        GameType type = gameState.getGameType();

        // Not very good, but whatever.
        String sideStr = gameState.getMySide().toString().toLowerCase();
        String typeStr = type.toString().toLowerCase();

        String cardRuleStr = "?";
        switch (gameState.getSpecialCardRule()) {
            case CLASSIC:
                cardRuleStr = "Classic";
                break;
            case FELLOWSHIPS_CHOICE:
                cardRuleStr = "Fellowship's choice";
                break;
            case TWO_OUT_OF_FOUR:
                cardRuleStr = "2 out of 4";
                break;
        }

        String shelobRuleStr = "?";
        switch (gameState.getShelobRule()) {
            case SHELOB_DIES:
                shelobRuleStr = "Shelob DIES";
                break;
            case SHELOB_FIGHTS:
                shelobRuleStr = "Shelob FIGHTS";
                break;
            case SHELOB_STAYS:
                shelobRuleStr = "Shelob STAYS";
                break;
        }

        // TODO: internationalize
        String messageBase = "<html>You are about to face <b>%s</b>. <br>" +
                "You will start as the <b>%s</b> player. <br><br>" +
                "The selected game type is a <b>%s</b> game. <br>" +
                "Special card rule: <b>%s</b>. <br>" +
                "Shelob rule: <b>%s</b>.</html>";
        String message = String.format(messageBase,
                gameState.getOpponentName(),
                sideStr,
                typeStr,
                cardRuleStr,
                shelobRuleStr);

        JOptionPane.showMessageDialog(mw, message, "Game about to begin", JOptionPane.PLAIN_MESSAGE,
                Portraits.randomIcon(gameState.getMySide()));

    }

    private void startGame()
    {

        assert gameState != null;

        mw.setTitle(Application.APP_NAME + " - " + gameState.getMyName() + " vs. " + gameState.getOpponentName());

        mw.showBoard(gameState.getMySide());

mw.setDecks(gameState.getMyDeck(), gameState.getOpponentDeck());


        showGameInformationDialog(gameState);

        List<EActor> characters;
        if (gameState.getGameType() == GameType.DRAFT) {
            characters = DraftGameDialog.show(mw, gameState.getMySide());
        } else if (gameState.getGameType() == GameType.CLASSIC) {
            characters = Arrays.asList(gameState.getMySide() == Side.LIGHT ? EActor.CLASSIC_LIGHT : EActor.CLASSIC_DARK);
        } else {
            characters = Arrays.asList(gameState.getMySide() == Side.LIGHT ? EActor.VARIANT_LIGHT : EActor.VARIANT_DARK);
        }

        gameState.setNumberOfSpecialCards(2);
        if (gameState.getSpecialCardRule() == SpecialCardRule.FELLOWSHIPS_CHOICE) {
            Integer numCards = null;
            if (gameState.getMySide() == Side.LIGHT) {
                ESpecialCard[] selectedCards;
                while (numCards == null) {
                    numCards = FellowshipsChoiceDialog.show(mw);
                    gameState.setNumberOfSpecialCards(numCards);
                }

                conn.sendMessage(new Message(Message.Type.NUMBER_OF_SPECIAL_CARDS, numCards));

                if (numCards.intValue() == 0) {
                    selectedCards = new ESpecialCard[0];
                } else if (numCards.intValue() == 4) {
                    selectedCards =
                            gameState.getMySide() == Side.LIGHT ? ESpecialCard.LIGHT_CARDS : ESpecialCard.DARK_CARDS;
                } else {
                    SpecialCardChooserDialog dlg = new SpecialCardChooserDialog(mw,
                            gameState.getMySide(), gameState.getNumberOfSpecialCards());
                    dlg.setVisible(true);

                    selectedCards = dlg.getSelectedCards();
                }
                gameState.setMySpecialCards(selectedCards);
                conn.sendMessage(new Message(Message.Type.SPECIAL_CARD_SELECTION, selectedCards));

                setupPlacementMode(characters);
            } else {
                waitForNumberOfCards(characters);
            }
        } else if (gameState.getSpecialCardRule() == SpecialCardRule.TWO_OUT_OF_FOUR) {
            SpecialCardChooserDialog dlg = new SpecialCardChooserDialog(mw,
                    gameState.getMySide(), gameState.getNumberOfSpecialCards());
            dlg.setVisible(true);

            gameState.setMySpecialCards(dlg.getSelectedCards());
            conn.sendMessage(new Message(Message.Type.SPECIAL_CARD_SELECTION, dlg.getSelectedCards()));

            setupPlacementMode(characters);
        } else if (gameState.getSpecialCardRule() == SpecialCardRule.CLASSIC) {
            // If classic mode, don't send anything, just move on.
            if (gameState.getMySide() == Side.LIGHT) {
                gameState.setMySpecialCards(new ESpecialCard[]{ESpecialCard.GANDALF_THE_WHITE, ESpecialCard.SHADOWFAX});
            } else {
                gameState.setMySpecialCards(new ESpecialCard[]{ESpecialCard.PALANTIR, ESpecialCard.RECALL_TO_MORDOR});
            }
            setupPlacementMode(characters);
        } else {
            throw new RuntimeException("Bad special card rule! " + gameState.getSpecialCardRule());
        }
    }

    private void waitForNumberOfCards(final List<EActor> characters)
    {

        mw.showCenteredWaitMessage("Fellowship's Choice - please wait...");

        SwingWorker<Void, Void> w = new SwingWorker<Void, Void>()
        {

            @Override
            protected Void doInBackground() throws Exception
            {
                Message m = conn.waitForNextMessage();
                assert m.dataIsOfClass(Integer.class);
                gameState.setNumberOfSpecialCards((Integer) m.getData());
                return null;
            }

            @Override
            protected void done()
            {
                mw.hideCenteredWaitMessage();

                ESpecialCard[] selected;
                int num = gameState.getNumberOfSpecialCards();
                System.out.println("Got num:  " + num);
                if (num == 0) {
                    selected = new ESpecialCard[0];
                    MessageBox.ok(mw,
                            String.format("<html>%s has elected to not use any special cards in this round!</html>", gameState.getOpponentName()),
                            "No special cards!", null, true);
                } else if (num == 4) {
                    selected =
                            gameState.getMySide() == Side.LIGHT ? ESpecialCard.LIGHT_CARDS : ESpecialCard.DARK_CARDS;
                    MessageBox.ok(mw,
                            String.format("<html>%s has elected to use <i>all</i> special cards in this round!</html>", gameState.getOpponentName()),
                            "All special cards!", null, true);
                } else {
                    SpecialCardChooserDialog dlg = new SpecialCardChooserDialog(mw,
                            gameState.getMySide(), gameState.getNumberOfSpecialCards());
                    dlg.setVisible(true);
                    selected = dlg.getSelectedCards();

                }
                gameState.setMySpecialCards(selected);
                conn.sendMessage(new Message(Message.Type.SPECIAL_CARD_SELECTION, selected));
                setupPlacementMode(characters);
            }
        };
        w.execute();

    }

    private void setupPlacementMode(List<EActor> characters)
    {

        assert gameState != null;

        ETerritory[] validTerritories;
        if (gameState.getMySide() == Side.LIGHT) {
            validTerritories = new ETerritory[]{SHIRE, ARTHEDAIN, CARDOLAN, RHUDAUR, EREGION, ENEDWAITH};
        } else {
            validTerritories = new ETerritory[]{MORDOR, GONDOR, DAGORLAD, ROHAN, FANGORN, MIRKWOOD};
        }

        // To get the pieces in top to bottom alphabetical order.
        Collections.sort(characters, new Comparator<EActor>()
        {

            public int compare(EActor o1, EActor o2)
            {
                return Actors.getPieceName(o1).compareTo(Actors.getPieceName(o2));
            }
        });

        // Prepare default positions for unplaced pieces.
        Map<EActor, Point> unplacedPositions = new EnumMap<EActor, Point>(EActor.class);
        int y = 260;
        for (EActor ea : characters) {
            unplacedPositions.put(ea, new Point(700 - GamePiece.PIECE_WIDTH / 2, y));
            y += 7 + GamePiece.PIECE_HEIGHT;
        }

        // Prepare the pieces to be used in placement mode.
        Map<EActor, GamePiece> pieces = new EnumMap<EActor, GamePiece>(EActor.class);
        for (EActor ea : characters) {
            GamePiece gp = ea.isLight()
                    ? new LightPiece(ea, Actors.getPieceName(ea))
                    : new DarkPiece(ea, Actors.getPieceName(ea));
            gp.setLocation(unplacedPositions.get(ea));
            gp.remember();
            pieces.put(ea, gp);
        }

        ActorInfoPanel actorViewer = new ActorInfoPanel("?", "?", 0, Portraits.getDefault(), Side.LIGHT);
        actorViewer.setSize(260, 240);

        Rectangle unplacedRect = new Rectangle(600, 240, 200, 400);

        PlacementPanel pp = new PlacementPanel(mw, pieces.values(), validTerritories, actorViewer, unplacedPositions);
        PlacementHandler controller = new PlacementHandler(pp, unplacedRect, pieces.values(), validTerritories, unplacedPositions);
        controller.addListener(this);
        for (GamePiece gp : pieces.values()) {
            gp.addGamePieceListener(controller);
        }

        mw.setTemporaryPanel(pp);
    }

    public void placementComplete(Map<ETerritory, List<EActor>> placement)
    {
        mw.restorePanel();

        // Create a new WorldState and PieceCollection from the given placement.
        // Add the pieces to the map screen.
        // Order the pieces.
        List<GamePiece> pieces = new ArrayList<GamePiece>();
        ws = new WorldState();
        for (ETerritory et : placement.keySet()) {
            List<EActor> actors = placement.get(et);
            if (!actors.isEmpty()) {
                for (EActor ea : actors) {
                    ws.placeActor(ea, et);
                    GamePiece gp;
                    if (ea.isLight()) {
                        gp = new LightPiece(ea, Actors.getPieceName(ea));
                    } else {
                        gp = new DarkPiece(ea, Actors.getPieceName(ea));
                    }
                    gp.setMovable(false);
                    pieces.add(gp);
                }
            }
        }

        pieceColl = new PieceCollection(pieces);
        PiecePositioner.setShufflePiecesForSide(gameState.getMySide(), false);
        PiecePositioner.orderAll(pieceColl, ws);

        mw.addPiecesFromCollection(pieceColl);

        // Send my placement to opponent.
        conn.sendMessage(new Message(Message.Type.PLACEMENT, new PlacementPayload(placement)));

        // Before we wait for our opponent's placement, we should wait for his special card selection.
        // This should be in the message queue since we were in startGame (if 2 out of 4 rule is used),
        // or in waitForNumberOfCards (if Fellowship's choice).
        //
        // (This is NOT needed if we play with the CLASSIC SpecialCardRule. Then we use a default.)
        if (gameState.getSpecialCardRule() != SpecialCardRule.CLASSIC) {

            AsyncWaiter<ESpecialCard[]> specialCardWaiter =
                    new AsyncWaiter<ESpecialCard[]>(conn, ESpecialCard[].class,
                    mw, "Waiting for special card choice...");
            specialCardWaiter.addListener(new AsyncWaiter.Listener<ESpecialCard[]>()
            {

                public void completed(ESpecialCard[] cards)
                {
                    gameState.setOpponentSpecialCards(cards);
                    startPlacementWaiter();
                }
            });
            specialCardWaiter.execute();

        } else {

            ESpecialCard[] cards;
            if (gameState.getOpponentSide() == Side.DARK) {
                cards = new ESpecialCard[]{ESpecialCard.PALANTIR, ESpecialCard.RECALL_TO_MORDOR};
            } else {
                cards = new ESpecialCard[]{ESpecialCard.GANDALF_THE_WHITE, ESpecialCard.SHADOWFAX};
            }
            gameState.setOpponentSpecialCards(cards);
            startPlacementWaiter();
        }


    }

    private void startPlacementWaiter()
    {
        // Wait for opponent's placement.
        AsyncWaiter<PlacementPayload> placementWaiter =
                new AsyncWaiter<PlacementPayload>(conn, PlacementPayload.class,
                mw, "Waiting for opponent's placement...");
        placementWaiter.addListener(new AsyncWaiter.Listener<PlacementPayload>()
        {

            public void completed(PlacementPayload ppl)
            {
                gotOpponentPlacement(ppl);
            }
        });
        placementWaiter.execute();
    }

    private PieceCollection createNewPieceCollection(
            PieceCollection oldCollection, Map<ETerritory, List<EActor>> oppPlacement)
    {
        // Assemble a new piece collection of the already existing one
        // and the opponent's placement.
        // Also add the click listener that brings up the ActorInfoDialog.
        List<GamePiece> newList = new ArrayList<GamePiece>(oldCollection.values());

        for (ETerritory et : oppPlacement.keySet()) {
            for (EActor ea : oppPlacement.get(et)) {
                GamePiece gp;
                if (ea.isLight()) {
                    gp = new LightPiece(ea, Actors.getPieceName(ea));
                } else {
                    gp = new DarkPiece(ea, Actors.getPieceName(ea));
                }
                gp.setVisible(true);
                gp.setKnown(false);
                gp.setMovable(false);

                newList.add(gp);

                ws.placeActor(ea, et);
            }
        }

        return new PieceCollection(newList);
    }

    private void gotOpponentPlacement(PlacementPayload ppl)
    {

        mw.removePieces();

        Map<ETerritory, List<EActor>> oppPlacement = ppl.getPayload();
        pieceColl = createNewPieceCollection(pieceColl, oppPlacement);

        PiecePositioner.setShufflePiecesForSide(gameState.getOpponentSide(), true);
        PiecePositioner.orderAll(pieceColl, ws);

        mw.addPiecesFromCollection(pieceColl);

        mw.repaint();

        startGameLoop();
    }

    private void startGameLoop()
    {
        Game game = new Game(mw, menu, conn, config, gameState, ws, pieceColl);
        Thread t = new Thread(game);
        t.start();
    }
}
